The StepSecurity threat intelligence team has discovered an ongoing campaign in which an attacker is compromising hundreds of GitHub accounts and injecting identical malware into hundreds of Python repositories. The earliest injections date to March 8, 2026, and the campaign is still active with new repos continuing to be compromised. The attack targets Python projects — including Django apps, ML research code, Streamlit dashboards, and PyPI packages — by appending obfuscated code to files like setup.py, main.py, and app.py. Anyone who runs pip install from a compromised repo or clones and executes the code will trigger the malware.

The attacker gains access to developer accounts, takes the latest legitimate commit on each repo's default branch, rebases it with malicious code appended, and force-pushes — making it appear as if nothing changed. The commit message, author, and author date are all preserved from the original. We have filed security issues on the most notable affected repositories to notify maintainers.
This is an ongoing campaign. We are actively tracking this attack and will continue to update this blog post as new information becomes available. Some of these repositories may still contain the malicious code. If you use any Python packages installed directly from GitHub, check that the code on the default branch matches the last legitimate commit from the original author.
GitHub Search for Affected Repos
The full list of affected repositories can be found by searching GitHub for the malware's marker variable:
GitHub Code Search: lzcdrtfxyqiplpd

How the Attack Works

Phase 1: Account Takeover via GlassWorm Credential Theft
The account takeover mechanism has been identified: the compromised developers were infected with the GlassWorm malware through malicious VS Code and Cursor extensions. GlassWorm's stage 3 payload includes a dedicated credential theft module that harvests GitHub tokens from multiple sources: git credential fill, VS Code extension storage, ~/.git-credentials, and the GITHUB_TOKEN environment variable. Once the attacker has these credentials, they use them to force-push malware into all of the victim's repositories.
A Reddit user reported discovering that "null" had committed to most of their repos — and traced the infection back to a compromised Cursor extension . An independent analysis of the GlassWorm payload confirmed it steals GitHub and npm credentials, validates them against the GitHub API, and exfiltrates them to attacker-controlled servers.
The evidence for account-level compromise is clear: when an account with multiple repositories is taken, every repo under that account gets injected. For example, user BierOne has had 6 repos compromised, the organization wecode-bootcamp-korea has had 6 repos hit, and HydroRoll-Team has had 6.
Phase 2: Stealth Injection via Force-Push
The injection method is sophisticated. Rather than opening a pull request or creating a new commit (both of which would be visible in the repo's activity feed), the attacker:
- Takes the latest legitimate commit on the default branch
- Rebases it, appending obfuscated malware to a key Python file (
setup.py,main.py,app.py, etc.) - Force-pushes to the default branch
The commit message and author date are preserved from the original commit — only the committer date reveals the tampering. The committer email is also set to the string "null" across many of the malicious commits, which appears to be a fingerprint of the attacker's tooling.

Here are the date discrepancies we found across the most notable repositories:
amirasaran/request_validator— author date: 2017-04-24, committer date: 2026-03-10 (9 year gap)BierOne/relation-vqa— author date: 2019-04-11, committer date: 2026-03-13 (7 year gap)BierOne/bottom-up-attention-vqa— author date: 2021-06-01, committer date: 2026-03-13 (5 year gap)biodatlab/siriraj-assist— author date: 2024-03-19, committer date: 2026-03-13 (2 year gap)amirasaran/django-restful-admin— author date: 2024-11-15, committer date: 2026-03-10 (16 month gap)uknfire/tsmpy— author date: 2025-06-04, committer date: 2026-03-08 (9 month gap)KeithSloan/ImportNURBS— author date: 2025-08-28, committer date: 2026-03-10 (6 month gap)BierOne/ood_coverage— author date: 2024-10-25, committer date: 2026-03-12 (5 month gap)KeithSloan/GDML— author date: 2026-02-06, committer date: 2026-03-11 (33 day gap)
The GitHub Events API captures push events with before and after commit SHAs. For amirasaran/django-restful-admin, we can see the exact moment the default branch was replaced:
// March 10, 21:58 UTC — master force-pushed with malicious code
{
"type": "PushEvent",
"actor": "amirasaran", // compromised account
"created_at": "2026-03-10T21:58:02Z",
"ref": "refs/heads/master",
"before": "260ca635...", // clean commit (legitimate PR #16 merge)
"after": "17849e1b..." // malicious rebased commit
}

The before SHA (260ca635) is the legitimate merge commit from PR #16. The after SHA (17849e1b) is the attacker's rebased commit with malware appended to setup.py. Because the attacker uses the compromised account's own credentials, the push appears to come from the repo owner.

Phase 3: Solana Blockchain C2
The injected code is appended to the end of whatever Python file the attacker targets. It's obfuscated with three layers: base64 decoding, zlib decompression, and XOR decryption (key: 134). The variable names are randomized 15-character strings, but the base64 payload blob is identical across all compromised repos, stored in a variable named lzcdrtfxyqiplpd.
# Obfuscation wrapper (appended to end of legitimate Python file)
# -*- coding: utf-8 -*-
aqgqzxkfjzbdnhz = __import__('base64')
wogyjaaijwqbpxe = __import__('zlib')
idzextbcjbgkdih = 134
qyrrhmmwrhaknyf = lambda d, o: bytes([b ^ idzextbcjbgkdih for b in d])
lzcdrtfxyqiplpd = '<4,800-character base64 blob>'
runzmcxgusiurqv = wogyjaaijwqbpxe.decompress(
aqgqzxkfjzbdnhz.b64decode(lzcdrtfxyqiplpd))
ycqljtcxxkyiplo = qyrrhmmwrhaknyf(runzmcxgusiurqv, idzextbcjbgkdih)
exec(compile(ycqljtcxxkyiplo, '<>', 'exec'))
After deobfuscation, the malware executes the following sequence:
The malware first checks if the system is Russian — examining locale settings, timezone, and UTC offset. If the system is Russian, execution is skipped entirely. This is a well-known pattern in Eastern European cybercrime operations to avoid targeting domestic systems.
# Comments in the deobfuscated code are in Russian:
# "Эмуляция объекта Buffer из Node.js" (Emulation of Node.js Buffer object)
# "Получение подписей для адреса Solana" (Getting signatures for Solana address)
# "Проверка, находится ли система в России" (Checking if system is in Russia)
Instead of connecting to a traditional C2 server that could be taken down, the malware reads its instructions from the Solana blockchain. It queries a specific Solana address for transaction memos containing JSON data with a payload URL:
Solana C2 address: BjVeAjPrSKFiingBn4vZvghsGj9KCE8AJVtbc9S8o8SC
The malware tries 9 different Solana RPC endpoints as fallbacks, making it highly resilient to any single endpoint being blocked:
api.mainnet-beta.solana.comsolana-mainnet.gateway.tatum.iogo.getblock.ussolana-rpc.publicnode.comapi.blockeden.xyzsolana.drpc.orgsolana.leorpc.comsolana.api.onfinality.iosolana.api.pocket.network
Using the blockchain as a C2 channel means the attacker can update the payload URL at any time by posting a new transaction — and no one can delete or censor the instructions once they're on-chain.

Phase 4: Payload Execution
Once the malware retrieves the payload URL from the Solana memo, it:
- Downloads Node.js v22.9.0 from
nodejs.orgto the user's home directory (cross-platform: Windows/macOS/Linux, x64/ARM) - Fetches the encrypted JavaScript payload from the URL, receiving an IV and secret key in HTTP response headers
- Writes a JS file (
i.js) that decrypts and executes the payload via the downloaded Node.js - Creates a persistence file (
~/init.json) with a 2-day recheck timer to avoid repeated execution
The final JS payload is encrypted with AES, making static analysis of the second stage impossible without the server-side key. Based on the infrastructure pattern (Solana C2 + Node.js execution + AES encryption + CIS exclusion), this is consistent with known crypto wallet stealer / infostealer campaigns.
Harden-Runner Analysis: Catching the Malware in Action
To confirm the malware behavior, we ran the compromised setup.py from amirasaran/django-restful-admin in a controlled GitHub Actions environment with StepSecurity Harden-Runner monitoring all network activity. The results confirm the full attack chain described above.
Within seconds of executing python3 setup.py, Harden-Runner captured the following network activity from the python3.12 process:
- Solana C2 query (T+10s) — DNS resolution of
api.mainnet-beta.solana.com(208.91.111.195:443) — the malware querying the blockchain for its C2 instructions - Payload URL fetch (T+20s) — Connection to
217.69.0.159:80— the URL decoded from the Solana transaction memo - Node.js download (T+21s) — DNS resolution of
nodejs.org(172.66.128.70:443) — downloading Node.js v22.9.0 to execute the encrypted JavaScript payload - Node.js extracted (T+24s) —
/home/runner/node-v22.9.0-linux-x64/bin/nodedeployed — Harden-Runner automatically detected the new binary and attached TLS monitoring


None of these connections belong in a Python project's CI/CD pipeline. A setup.py has no legitimate reason to contact Solana RPC endpoints, download Node.js, or connect to unknown IPs. Harden-Runner's network egress monitoring flags exactly this kind of anomalous activity. Add Harden-Runner to your workflows to detect compromised dependencies before they can exfiltrate data.
The full workflow run with Harden-Runner insights is available at: StepSecurity Insights Dashboard.
Solana C2 On-Chain Analysis
Because the attacker uses the Solana blockchain as a C2 channel, the full history of C2 instructions is permanently recorded on-chain and available for analysis. We queried the C2 address BjVeAjPrSKFiingBn4vZvghsGj9KCE8AJVtbc9S8o8SC and decoded all transaction memos.
The C2 address is a wallet, not a smart contract
The attacker owns the private key for this address and uses Solana's Memo program to post JSON instructions. The malware on victim machines queries this address via Solana RPC, reads the latest memo, decodes the base64 link field, and connects to the resulting URL. The blockchain only records the attacker's side — victim connections happen off-chain to the payload servers.
The campaign predates the GitHub injections
The earliest transaction on the C2 address dates to November 27, 2025 — over three months before the first GitHub repo injections on March 8, 2026. The address has 50 transactions total, with the attacker regularly updating the payload URL, sometimes multiple times per day. This suggests the attacker was targeting other infection vectors before pivoting to GitHub repos.
Decoded payload URLs reveal 6 C2 server IPs
Each memo contains a base64-encoded HTTP URL pointing to the current payload server. Decoding all memos reveals the attacker has rotated through 6 different server IPs over the campaign's lifetime:
45.32.151.157— active December 2025 (earliest)45.32.150.97— active February 2026217.69.11.57— active February 2026217.69.11.99— active February–March 2026217.69.0.159— active March 13, 2026 (current — confirmed by Harden-Runner)45.76.44.240— active March 13, 2026
The 45.x IPs are in the Vultr hosting range. The 217.69.x IPs are in a Russian hosting range — consistent with the CIS exclusion behavior and Russian-language code comments in the malware.
Direct C2 configuration exposed on-chain
One transaction from February 25, 2026 contains a direct C2 configuration instead of a payload link — likely posted during a testing or reconfiguration phase:
{
"c2server": "http://217.69.11.99:5000", // Main C2 server
"checkIp": "http://217.69.11.99", // Victim IP fingerprinting
"dht_data": "217.69.11.99:10000" // DHT peer-to-peer fallback
}
This reveals three components of the attacker's infrastructure: a C2 server on port 5000, a victim fingerprinting endpoint (likely checking the victim's IP and geolocation before serving a payload), and a DHT (Distributed Hash Table) node on port 10000 — a peer-to-peer fallback that makes the C2 infrastructure even more resilient to takedowns.
Funding trail
The C2 address was funded on November 27, 2025 by wallet G2YxRa6wt1qePMwfJzdXZG62ej4qaTC7YURzuh2Lwd3t, which currently holds ~495 SOL. The C2 address itself holds only 0.006 SOL — just enough for transaction fees to post memo updates.
Connection to the GlassWorm Campaign
The Solana C2 address used by ForceMemo - BjVeAjPrSKFiingBn4vZvghsGj9KCE8AJVtbc9S8o8SC - is the same wallet address used by the GlassWorm malware campaign, a self-propagating worm that has been targeting VS Code and OpenVSX extensions since October 2025. This was first documented by Koi Security in their analysis of GlassWorm's Wave 4 (December 2025), which pivoted to macOS with trojanized crypto wallets.
The overlap is not just the wallet. Aikido Security reported that the GlassWorm actor compromised 151+ GitHub repositories between March 3–9, 2026 using invisible Unicode characters to hide payloads - with the decoded payload fetching its C2 instructions from the same Solana wallet. This means two parallel waves were hitting GitHub repos in early March 2026 from the same actor:
- GlassWorm wave (March 3–9): 151+ repos, invisible Unicode injection - reported by Aikido
- ForceMemo wave (March 8–13): 240+ repos, account takeover + force-push with obfuscated Python - reported by StepSecurity (this blog)
The two campaigns use different delivery methods and different code obfuscation (Unicode steganography vs. base64/zlib/XOR), but share the same Solana C2 infrastructure. This strongly suggests ForceMemo is a new delivery vector operated by the GlassWorm threat actor, expanding from VS Code extensions to mass GitHub account takeover.
What the attacker is likely stealing
While we cannot see the final JavaScript payload (it is AES-encrypted with a key delivered via HTTP headers), the infrastructure pattern is consistent with known crypto wallet stealer / infostealer campaigns:
- CIS country exclusion — classic Eastern European cybercrime operational security
- Node.js runtime — ideal for accessing browser extension data (crypto wallets like MetaMask, Phantom), stored credentials, and cookies
- AES encryption of payloads — prevents static analysis and signature-based detection
- Victim IP fingerprinting (
checkIpendpoint) — allows the attacker to filter or customize payloads per victim - 2-day persistence timer — designed for sustained access, not one-time exfiltration
The most likely targets are browser crypto wallet extensions (seed phrases, private keys), stored credentials and session cookies, and SSH keys.
Scope of the Campaign
So far, we have identified hundreds of Python repositories across hundreds of GitHub accounts injected with identical malware, and the number continues to grow. The targeted repos include Django web applications, machine learning research code, Streamlit dashboards, Flask APIs, and Python packages installable via pip install from GitHub URLs. Several of the compromised repos have setup.py files — meaning a pip install directly from the repo executes the malware during installation.
The full list of affected repositories can be found by searching GitHub for the malware's marker variable:
GitHub Code Search: lzcdrtfxyqiplpd

Account-Level Compromise: Repeat Victims
The strongest evidence for account-level compromise is the pattern of multiple repositories being hit per account. These numbers are growing as the campaign continues:
wecode-bootcamp-korea(Organization) — 6 repos compromisedHydroRoll-Team(Organization) — 6 repos compromisedBierOne(User) — 6+ repos compromisedgnlxpy(User) — 6 repos compromisedFo2sh88(User) — 6 repos compromisedwatercrawl(Organization) — 4 repos compromisedtavasolireza(User) — 4 repos compromisedBishalBudhathoki(User) — 4 repos compromisediperformance(User) — 4 repos compromisedamirasaran(User) — 3 repos compromisedKeithSloan(User) — 2 repos compromised
File Types Targeted
The attacker's tooling selects the most prominent Python entry point in each repo:
main.py— most common target (~70 repos)setup.py— triggers onpip install .(~25 repos)app.py— Flask/Streamlit apps (~25 repos)manage.py— Django projects (~20 repos)app/__init__.py— package init files (~8 repos)- Various:
streamlit_app.py,run.py,config.py,cli.py,noxfile.py
Timeline
Campaign Timeline
- November 27, 2025 — Earliest activity on the Solana C2 address. Funding wallet
G2YxRa...transfers SOL and first payload URLs point to45.32.151.157. The attacker is likely targeting other infection vectors before GitHub repos. - December 2025 – February 2026 — Attacker rotates through payload servers (
45.32.151.157,45.32.150.97,217.69.11.57,217.69.11.99), posting ~40 memo transactions with updated payload URLs. - March 8 — Earliest GitHub repo injections detected:
metalogico/issued,uknfire/tsmpy,gnlxpy/*repos,wecode-bootcamp-korea/*repos - March 10 — Major wave:
amirasaran/*repos (including 70-star django-restful-admin),KeithSloan/ImportNURBS,watercrawl/*repos - March 11 — Continued:
KeithSloan/GDML - March 12 —
BierOne/ood_coverage(34-star ICLR paper) - March 13 — Latest wave:
BierOne/bottom-up-attention-vqa,BierOne/relation-vqa,biodatlab/siriraj-assist,HydroRoll-Team/HydroRoll - March 14 — First repos begin reverting (e.g.,
KeithSloan/GDMLrestored at 14:05 UTC). StepSecurity publishes initial findings and files security issues on affected repos. - Ongoing — Campaign remains active. We are continuing to monitor and will update this post.
Indicators of Compromise
- Solana C2 address:
BjVeAjPrSKFiingBn4vZvghsGj9KCE8AJVtbc9S8o8SC - Solana funding wallet:
G2YxRa6wt1qePMwfJzdXZG62ej4qaTC7YURzuh2Lwd3t - Marker variable in code:
lzcdrtfxyqiplpd - XOR decryption key:
134 - Committer email (fingerprint):
"null"(string) - Node.js version downloaded: v22.9.0
- Persistence file:
~/init.json - JS payload file:
i.js(in script directory) - Code comments language: Russian
- CIS exclusion: Skips execution on Russian locale/timezone
C2 Payload Server IPs (from on-chain analysis)
45.32.151.157 (active Dec 2025)
45.32.150.97 (active Feb 2026)
217.69.11.57 (active Feb 2026)
217.69.11.99 (active Feb–Mar 2026, C2 server on :5000, DHT on :10000)
217.69.0.159 (active Mar 2026 — current)
45.76.44.240 (active Mar 2026)
Solana RPC Endpoints Contacted
api.mainnet-beta.solana.com
solana-mainnet.gateway.tatum.io
go.getblock.us
solana-rpc.publicnode.com
api.blockeden.xyz
solana.drpc.org
solana.leorpc.com
solana.api.onfinality.io
solana.api.pocket.network
Why We Call It ForceMemo
We are tracking this campaign as ForceMemo, derived from its two most distinctive technical artifacts:
- Force — the attacker injects malware by force-pushing to the default branch of compromised repositories. This technique rewrites git history, preserves the original commit message and author, and leaves no pull request or commit trail in GitHub's UI. No other documented supply chain campaign uses this injection method.
- Memo — the malware uses Solana blockchain transaction memos as its command-and-control channel, reading payload URLs from memo data attached to transactions on a specific Solana address. This makes the C2 instructions immutable and censorship-resistant.
How to Check If You're Affected
If you install Python packages directly from GitHub (e.g., pip install git+https://github.com/...) or clone and run Python repos:
- Search for the marker variable in any Python files you've cloned:
grep -r "lzcdrtfxyqiplpd" . - Check for
~/init.jsonon your system — this is the malware's persistence file - Check for downloaded Node.js in your home directory:
ls ~/node-v22* - Check for
i.jsin any recently-cloned project directories - Review git commit history of repos you've cloned — look for commits where the committer date is significantly newer than the author date
Disclosure
We filed security issues on the most notable affected repositories to notify maintainers:
- amirasaran/django-restful-admin #17
- amirasaran/request_validator #3
- BierOne/bottom-up-attention-vqa #9
- BierOne/ood_coverage #6
- metalogico/issued #7
- biodatlab/siriraj-assist #2
- KeithSloan/ImportNURBS #15
StepSecurity Threat Intelligence
StepSecurity threat intelligence was the first to discover and publicly report on this campaign. The team is actively monitoring the situation and will continue to update this post. StepSecurity continuously monitors the open source and CI/CD ecosystem for emerging threats — including supply chain attacks, compromised GitHub Actions, malicious packages, and account takeover campaigns like this one.
StepSecurity customers receive threat intelligence alerts directly in their dashboard, with actionable guidance on whether they are affected and how to remediate.
How Harden-Runner Detects This Attack
Harden-Runner monitors network traffic, file system changes, and process activity on GitHub Actions runners. It is designed to catch exactly the kind of anomalous behavior that supply chain attacks like ForceMemo produce.
When we ran the compromised setup.py with Harden-Runner in audit mode, it captured every outbound connection the malware made:
api.mainnet-beta.solana.com:443— Solana blockchain C2 query217.69.0.159:80— Payload URL fetched from the Solana memonodejs.org:443— Node.js v22.9.0 download
All three connections were flagged as "Not in baseline" — meaning they had never been seen in any previous run of the workflow. A Python setup.py has no legitimate reason to contact Solana RPC endpoints, download Node.js, or connect to unknown IPs.
Once a baseline is established, Harden-Runner can block any outbound connection that is not in the allowed list. This would have prevented the malware from reaching the Solana C2, downloading Node.js, or exfiltrating data.




